/**
 * 
 * @Project le cOeur - Data Virtualization
 * @Date 27.11.2012
 * @Author Aebi Pascal, Kocher Marc
 * @School BFH-TI Biel, Bachelor Thesis
 *
 */

package main.framework.resource;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;

import main.framework.exception.MethodNotFoundException;
import main.framework.exception.ResourceNotFoundException;
import main.framework.exception.RootElementNotFoundException;
import main.framework.http.Method;
import main.framework.http.Request;
import main.framework.setting.Config;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

public class ResourceManager {
	
	private Resource rootResource;
	private String additionalRootResourcePath;
	private Resource lastResourceOfResourcePath;

	/**
	 * Creates a new manager for the resources which he gets by parsing an XML file.
	 * 
	 * @param rootResourcePath The additional root resource path
	 * @throws URISyntaxException If the given string violates RFC 2396, as augmented by the above deviations.
	 */
	public ResourceManager(String rootResourcePath) throws URISyntaxException {
		this.additionalRootResourcePath = rootResourcePath;
		Resource parentResource = null;
		boolean rootResourceCreated = false;
		if(!rootResourcePath.isEmpty()){
			String[] pathSegments = rootResourcePath.split("/");
			for (int i = 0; i < pathSegments.length; i++) {
				if(!pathSegments[i].isEmpty()){
					if(rootResourceCreated){
						Resource resource = new Resource(new ResourceURI(new URI(parentResource.getResourceURI().getURI() + "/" + pathSegments[i])));
						parentResource.addChild(resource);
						parentResource = resource;
						if(i == pathSegments.length-1){
							this.lastResourceOfResourcePath = resource;
						}
					}
					else{
						parentResource = new Resource(new ResourceURI(new URI("/" + pathSegments[i])));
						this.rootResource = parentResource;
						this.lastResourceOfResourcePath = parentResource;
						rootResourceCreated = true;
					}
				}
			}
		}
	}

	/**
	 * Parses an XML file to create a document and parses this document.
	 * 
	 * @param filePath The XML file with its path.
	 * @throws ParserConfigurationException If a DocumentBuilder cannot be created which satisfies the configuration requested.
	 * @throws SAXException If any parse errors occur.
	 * @throws IOException If any IO errors occur.
	 * @throws RootElementNotFoundException If the root element is not found.
	 * @throws InstantiationException If this Class represents an abstract class, an interface, an array class, a primitive type, or void; or if the class has no nullary constructor; or if the instantiation fails for some other reason.
	 * @throws IllegalAccessException If the class or its nullary constructor is not accessible.
	 * @throws ClassNotFoundException If the class cannot be located.
	 * @throws URISyntaxException If the given string violates RFC 2396.
	 */
	public void parseXMLFile(String filePath) throws ParserConfigurationException, SAXException, IOException, RootElementNotFoundException, InstantiationException, IllegalAccessException, ClassNotFoundException, URISyntaxException {
		// validate the XML file with the XML schema
		this.validateXMLFile(filePath);
		// get the document builder factory
		DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
		// using factory get an instance of document builder
		DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
		// parse using builder to get DOM representation of the XML file
		Document document = documentBuilder.parse(filePath);
		// get root element and throw exception if it does not exist
		Element documentElement = document.getDocumentElement();
		if(documentElement == null) throw new RootElementNotFoundException("Root element not found.");
		// create root resource and parse the document
		Node rootNode = (Node) documentElement;
		Resource xmlRootResource = this.createResource((Element) rootNode, null);
		this.parseDocument(rootNode, xmlRootResource);
		if(this.rootResource == null){
			this.lastResourceOfResourcePath = xmlRootResource;
			this.rootResource = this.lastResourceOfResourcePath;
		}
		else{
			this.lastResourceOfResourcePath.addChild(xmlRootResource);
		}
	}
	
	/**
	 * Validates an XML file against an XML Schema Definition
	 * 
	 * @param filePath The XML file with its path.
	 * @throws SAXException If any parse errors occur.
	 * @throws IOException If any IO errors occur.
	 */
	private void validateXMLFile(String filePath) throws SAXException, IOException {
		 // define the type of schema: W3C
	      String schemaLang = "http://www.w3.org/2001/XMLSchema";
	      // get validation driver
	      SchemaFactory factory = SchemaFactory.newInstance(schemaLang);
	      // create schema by reading it from an XSD file
	      Schema schema = factory.newSchema(new StreamSource(Config.RESOURCE_XSD_FILE_PATH));
	      Validator validator = schema.newValidator();
	      // at last perform validation
	      validator.validate(new StreamSource(filePath));
	}
	
	/**
	 * Parses the document elements of a given node and his children.
	 * 
	 * @param parentNode The parent node from which to get all children.
	 * @param parentResource The parent resource from which to start the parsing.
	 * @throws URISyntaxException If the given string violates RFC 2396.
	 * @throws InstantiationException If this Class represents an abstract class, an interface, an array class, a primitive type, or void; or if the class has no nullary constructor; or if the instantiation fails for some other reason.
	 * @throws IllegalAccessException If the class or its nullary constructor is not accessible.
	 * @throws ClassNotFoundException If the class cannot be located.
	 */
	private void parseDocument(Node parentNode, Resource parentResource) throws InstantiationException, IllegalAccessException, ClassNotFoundException, URISyntaxException {
		// get a nodelist of children
		NodeList childNodeList = parentNode.getChildNodes();
		// parse nodelist if there exist children
		if(childNodeList != null && childNodeList.getLength() > 0){
			for(int i = 0 ; i < childNodeList.getLength(); i++){
				Node childNode = childNodeList.item(i);
				// create resource object if current child node is a valid element, add it to parent and parse the child node
				if(childNode instanceof Element){
					Element element = (Element)childNode;
					Resource resource = this.createResource(element, parentResource);
					parentResource.addChild(resource);
					this.parseDocument(childNode, resource);
				}
			}
		}
	}
	
	/**
	 * Checks if the desired options in the HTTP request are available.
	 * 
	 * @param request The HTTP request to be checked.
	 * @return The result of the HTTP request check.
	 * @throws MethodNotFoundException The path is true but the requested HTTP method is not available for this resource.
	 * @throws ResourceNotFoundException If the resource manager has not found anything matching the requested URI.
	 */
	public boolean checkRequest(Request request) throws MethodNotFoundException, ResourceNotFoundException {
		boolean checkState = false;
		ResourceURI resourceURI = new ResourceURI(request.getRequestedURI());
		ResourceURINode resourceURINode = resourceURI.getFirstNode();
		if(resourceURINode != null && this.rootResource.getResourceURI().getFirstNode().equals(resourceURINode)){
			// if it is the last uri node, the next is the trailer so we have to check the next of the next
			if(resourceURINode.getNext().getNext() != null){
				checkState = this.checkHTTPRequest(this.rootResource.getChildren(), resourceURINode.getNext(), request.getMethod());
			}
			else{
				checkState = true;
			}
		}
		if(checkState == false) throw new ResourceNotFoundException("The resource manager has not found anything matching the requested URI.");
		return checkState;
	}
	
	/**
	 * Checks if the path of the URI is correct and if the requested HTTP method is available for this resource.
	 * 
	 * @param resourceURINode The current resource node to check.
	 * @param childResources The child resources of the current resource.
	 * @param method The requested HTTP method.
	 * @return The result of the path check.
	 * @throws MethodNotFoundException The path is true but the requested HTTP method not available for this resource.
	 */
	private boolean checkHTTPRequest(List<Resource> childResources, ResourceURINode resourceURINode, Method method) throws MethodNotFoundException {
		boolean checkState = false;
		for(Resource resource : childResources) {
			ResourceURINode currentURINode = resource.getResourceURI().getLastNode();
			if(currentURINode.equals(resourceURINode)){
				checkState = true;
				// if it is the last uri node, the next is the trailer so we have to check the next of the next
				if(resourceURINode.getNext().getNext() != null){
					return this.checkHTTPRequest(resource.getChildren(), resourceURINode.getNext(), method);
				}
				else{
					 if(resource.getResourceMethod(method) == null) throw new MethodNotFoundException("Error code 501 - The server does not support the functionality required to fulfill the request.");
				}
			}
		}
		return checkState;
	}
	
	/**
	 * Returns a specific resource.
	 * 
	 * @param uri The URI of the desired resource.
	 * @return The resource object which belongs to the given URI, or null if no resource with the given URI was found.
	 */
	public Resource getResource(URI uri) {
		Resource resource = null;
		ResourceURI resourceURI = new ResourceURI(uri);
		ResourceURINode resourceURINode = resourceURI.getFirstNode();
		if(resourceURINode != null && this.rootResource.getResourceURI().getFirstNode().equals(resourceURINode)){
			// if it is the last uri node, the next is the trailer so we have to check the next of the next
			if(resourceURINode.getNext().getNext() != null){
				resource = this.getResource(this.rootResource.getChildren(), resourceURINode.getNext());
			}
			else{
				resource = this.rootResource;
			}
		}
		return resource;
	}
	
	/**
	 * Searches recursively a resource with the given parameters.
	 * 
	 * @param childResources The child resource.
	 * @param resourceURINode The resource URI node.
	 * @return The searched resource.
	 */
	private Resource getResource(List<Resource> childResources, ResourceURINode resourceURINode) {
		Resource resource = null;
		for(Resource res : childResources) {
			ResourceURINode currentURINode = res.getResourceURI().getLastNode();
			if(currentURINode.equals(resourceURINode)){
				resource = res;
				// if it is the last uri node, the next is the trailer so we have to check the next of the next
				if(resourceURINode.getNext().getNext() != null){
					return this.getResource(res.getChildren(), resourceURINode.getNext());
				}
				else{
					// we are at the last level of the uri and the resource is the one we're looking for
					return resource;
				}
			}
		}
		return resource;
	}
	
	/**
	 * Returns the root resource of the web service.
	 * 
	 * @return The root resource.
	 */
	public Resource getRootResource() {
		return this.rootResource;
	}
	
	/**
	 * Creates a resource object.
	 * 
 	 * @param element The name of the resource method to be created.
	 * @param parentResource The parent of the resource to be created.
	 * @return The created resource.
	 * @throws URISyntaxException If the given string violates RFC 2396.
	 * @throws InstantiationException If this Class represents an abstract class, an interface, an array class, a primitive type, or void; or if the class has no nullary constructor; or if the instantiation fails for some other reason.
	 * @throws IllegalAccessException If the class or its nullary constructor is not accessible.
	 * @throws ClassNotFoundException If the class cannot be located.
	 */
	private Resource createResource(Element element, Resource parentResource) throws URISyntaxException, InstantiationException, IllegalAccessException, ClassNotFoundException {
		URI uri;
		String stringURI = element.getAttribute("uri");
		if(parentResource == null){
			if(this.additionalRootResourcePath.equals("/")){
				uri = new URI(stringURI);
			}
			else{
				uri = new URI(this.additionalRootResourcePath + stringURI);
			}
		}
		else{
			uri = new URI(parentResource.getResourceURI().getURI() + stringURI);
		}
		ResourceURI resourceURI = new ResourceURI(uri);
		String get = element.getAttribute("get");
		String put = element.getAttribute("put");
		String post = element.getAttribute("post");
		String delete = element.getAttribute("delete");
		Map<Method, ResourceMethod> resourceMethods = new HashMap<>();
		if (!get.isEmpty()) {
			resourceMethods.put(Method.GET, this.createResourceMethod(get));
		}
		if (!put.isEmpty()) {
			resourceMethods.put(Method.PUT, this.createResourceMethod(put));
		}
		if (!post.isEmpty()) {
			resourceMethods.put(Method.POST, this.createResourceMethod(post));
		}
		if (!delete.isEmpty()) {
			resourceMethods.put(Method.DELETE, this.createResourceMethod(delete));
		}
		return new Resource(resourceURI, resourceMethods);
	}
	
	/**
	 * Creates a resource method object.
	 * 
	 * @param methodName The name of the resource method to be created.
	 * @return The created resource method.
	 * @throws InstantiationException If this Class represents an abstract class, an interface, an array class, a primitive type, or void; or if the class has no nullary constructor; or if the instantiation fails for some other reason.
	 * @throws IllegalAccessException If the class or its nullary constructor is not accessible.
	 * @throws ClassNotFoundException If the class cannot be located.
	 */
	private ResourceMethod createResourceMethod(String methodName) throws InstantiationException, IllegalAccessException, ClassNotFoundException  {
		return (ResourceMethod) Class.forName(Config.RESOURCE_METHOD_PATH + "." + methodName).newInstance();
	}
	
}
